package org.jeecg.config.filter;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.security.DataSecurity;
import org.jeecg.common.security.DataSecurityLoadService;
import org.jeecg.common.util.DateUtils;
import org.jeecg.config.sign.util.BodyWriterHttpServletResponseWrapper;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 敏感信息加密
 * @author: qiang.zhou
 * @date: 2024/6/3 15:48
 **/
@Slf4j
public class DataSecurityFilter implements Filter, InitializingBean {

    private List<String> ignoreUrlList;
    private final DataSecurityLoadService dataSecurityLoadService;
    private final DataSecurity dataSecurity;

    /** 读取敏感信息正则库时使用 */
    private final Lock lock = new ReentrantLock();
    /** 预编译正则 */
    private Pattern pattern;
    /** 预编译正则过期时间 */
    private Date expireDate;

    /** 正则替换模板 */
    private final static String REPLACE_FORMAT = "$1$2%s$4";
    /** 敏感信息加密字段正则 */
    private final static String REGEX_FIELD_FORMAT = "(%s)";
    /** 敏感信息加密正则 */
    private final static String REGEX_SECURITY = "(=|=\\[|\\\"\\s*:\\s*\\\"|:|：|=')([0-9a-zA-Z\\u4e00-\\u9fa5`~!@#$\\%\\^&*()_\\-+=<>?:{}〔〕《》|.\\/;\\[\\]·~！￥%…（）—\\\\？：“”【】、；‘'。、]+)(\\]|\\\"|'|)";

    public DataSecurityFilter(DataSecurityLoadService dataSecurityLoadService, DataSecurity dataSecurity) {
        this(dataSecurityLoadService, dataSecurity, null);
    }

    public DataSecurityFilter(DataSecurityLoadService dataSecurityLoadService, DataSecurity dataSecurity, List<String> ignoreUrlList) {
        this.dataSecurityLoadService = dataSecurityLoadService;
        this.dataSecurity = dataSecurity;
        this.ignoreUrlList = ignoreUrlList;
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        this.loadConfig();
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        // 需要忽略的
        String url = request.getServletPath();
        if (this.checkMatch(ignoreUrlList, url)) {
            log.info("当前地址忽略敏感信息加密; path: {}", url);
            filterChain.doFilter(request, response);
            return;
        }
        BodyWriterHttpServletResponseWrapper responseWrapper = new BodyWriterHttpServletResponseWrapper(response);
        filterChain.doFilter(request, responseWrapper);
        String contentType = responseWrapper.getContentType();
        if (StringUtils.isBlank(contentType)) {
            log.info("contentType为空; 不加密数据");
            // 返回原始内容
            response.getOutputStream().write(responseWrapper.getContent());
            return;
        }

        // contextType=application/json 或 contextType=text/html
        if(contentType.contains(MediaType.APPLICATION_JSON_VALUE)  || contentType.contains(MediaType.TEXT_HTML_VALUE)) {
            log.info("将返回中包含的敏感信息加密");
            byte[] content = responseWrapper.getResponseData();
            content = this.encryptValue(content, url);
            this.response(content, servletResponse);
            log.info("已将返回中的敏感信息加密");
        } else {
            // 其他情况下返回原始内容
            response.getOutputStream().write(responseWrapper.getContent());
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.loadConfig();
    }

    private void loadConfig() {
        // 过期时间为空 或 过期时间在当前时间之前 都更新config配置
        if (null == pattern || null == this.expireDate || this.expireDate.before(new Date())) {
            lock.lock();
            try {
                List<String> fieldList = dataSecurityLoadService.loadSecurityField();
                log.info("敏感信息加密预编译正则库已到期，重新编译正则库；当前需要加密字段共{}个", fieldList.size());
                if (ObjectUtils.isEmpty(fieldList)) {
                    return;
                }
                String regex = String.format(REGEX_FIELD_FORMAT, String.join("|", fieldList)) + REGEX_SECURITY;
                this.pattern = Pattern.compile(regex);
                // 设置过期时间
                this.expireDate = DateUtils.calculate(new Date(), 30, Calendar.MINUTE);
            } catch (Exception e) {
                log.error("敏感信息加密预编译正则库编译失败, 错误消息: ", e);
            } finally {
                lock.unlock();
            }
        }
    }

    private byte[] encryptValue(byte[] content, String url) {
        if (null == this.pattern) {
            return content;
        }

        try {
            StringBuffer sb = new StringBuffer();
            Matcher matcher = this.pattern.matcher(new String(content));

            // 模仿Matcher.replaceAll方法, 将每个匹配地方的值加密后替换回去
            boolean result = matcher.find();
            if (result) {
                log.info("[{}]接口返回的数据中包含敏感信息, 将敏感信息加密", url);
                do {
                    // 3表示正则表达式中第三个分组, 每一个()为一个分组
                    String value = matcher.group(3);
                    matcher.appendReplacement(sb, String.format(REPLACE_FORMAT, dataSecurity.encrypt(value)));
                    result = matcher.find();
                } while (result);
                matcher.appendTail(sb);
                return sb.toString().getBytes(StandardCharsets.UTF_8);
            }
            return content;
        } catch (Exception e) {
            log.error("敏感信息加密失败,失败原因:{}", e.getMessage());
            return content;
        }
    }

    @SneakyThrows
    private void response(byte[] content, ServletResponse servletResponse) {
        OutputStream out = servletResponse.getOutputStream();
        out.write(content);
        out.flush();
    }

    public boolean checkMatch(List<String> paths, String path) {
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = false;
        Iterator var4 = paths.iterator();

        while(var4.hasNext()) {
            String configPath = (String)var4.next();
            match = configPath.startsWith("reg:") ? Pattern.matches(configPath.substring(4), path) : antPathMatcher.match(configPath, path);
            if (match) {
                break;
            }
        }

        return match;
    }

}
